/*
 * Copyright (c) 2023, Phytium Technology Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>

#include <boost/system/error_code.hpp>
#include <ipmid/api-types.hpp>
#include <ipmid/api.hpp>
#include <ipmid/handler.hpp>
#include <sdbusplus/exception.hpp>

#include <iomanip>
#include <iostream>
#include <string>
#include <variant>
#include <vector>

using namespace std;

namespace oem
{
constexpr uint8_t cmdOemWriteFru = 0x12;
} // namespace oem

static bool formatFru(const vector<uint8_t>& fruBytes)
{
    constexpr size_t fruBlockSize = 8;
    constexpr size_t maxFruDataSize = 512;

    if (fruBytes.size() <= fruBlockSize || fruBytes.size() > maxFruDataSize)
    {
        std::cerr << "Error FRU size\n";
        return false;
    }

    auto checkSum = [](const unsigned char* data, size_t len) {
        unsigned char checksum = 0;

        for (size_t byte = 0; byte < len; byte++)
        {
            checksum += *data++;
        }
        return checksum;
    };

    cout << "Receive FRU: " << endl;
    for (int i = 1; i <= fruBytes.size(); i++)
    {
        cout << hex << "0x" << setw(2) << setfill('0') << (int)fruBytes[i - 1]
             << " ";
        if (i % 16 == 0)
        {
            cout << endl;
        }
    }
    cout << endl;

    if (checkSum(fruBytes.data(), fruBlockSize))
    {
        cerr << "Error head checksum.\n";
        return false;
    }

    // Don't parse Internal and Multirecord areas.
    uint8_t count = 1;

    while (++count < 4)
    {
        if (!fruBytes[count])
        {
            continue;
        }

        size_t areaOffset = fruBytes[count] * fruBlockSize;

        if (checkSum(fruBytes.data() + areaOffset,
                     fruBytes[areaOffset + 1] * fruBlockSize))
        {
            cerr << "Error area checksum.\n";
            return false;
        }
    }

    return true;
}

ipmi::RspType<vector<uint8_t>>
    ipmiOemWriteFru(const uint8_t i2cBus, const uint8_t i2cDevice,
                    const uint8_t pageSize, const vector<uint8_t>& fruRawBinary)
{
    size_t fruSize = fruRawBinary.size();

    if (!formatFru(fruRawBinary))
    {
        return ipmi::responseReqDataLenInvalid();
    }

    auto bus = getSdBus();

    static auto setFruGpio = [bus](int32_t value) {
        auto method =
            bus->new_method_call("xyz.openbmc_project.Gpio.Manager",
                                 "/xyz/openbmc_project/gpio/RTC_SELECT_N",
                                 "org.freedesktop.DBus.Properties", "Set");
        method.append("xyz.openbmc_project.Gpio.Manager", "Value");
        method.append(variant<int32_t>(value));

        try
        {
            bus->call_noreply(method);
        }
        catch (const sdbusplus::exception::exception& e)
        {
            cerr << "Error select gpio.\n";
        }
    };

    // Switch the eeprom device to BMC through gpio.
    setFruGpio(1);

    string i2cPath("/dev/i2c-" + to_string(i2cBus));

    int fruOut = open(i2cPath.c_str(), O_RDWR | O_CLOEXEC);
    if (fruOut < 0)
    {
        setFruGpio(0);
        return ipmi::responseUnspecifiedError();
    }

    if (ioctl(fruOut, I2C_SLAVE_FORCE, i2cDevice) < 0)
    {
        std::cerr << "Set device address failed.\n";
        close(fruOut);
        setFruGpio(0);
        return ipmi::responseUnspecifiedError();
    }

    constexpr size_t addrSize = 2;
    size_t writeOffset = 0;
    vector<uint8_t> temporary(addrSize + pageSize, 0);

    for (size_t index = 1; fruSize > 0; index++)
    {
        if (fruSize <= pageSize)
        {
            memcpy(temporary.data() + addrSize,
                   fruRawBinary.data() + writeOffset, fruSize);

            if (write(fruOut, temporary.data(), fruSize + addrSize) !=
                fruSize + addrSize)
            {
                cerr << "Error write end data.\n";
                close(fruOut);
                setFruGpio(0);
                return ipmi::responseUnspecifiedError();
            }
            close(fruOut);

            bus->async_method_call(
                [bus](const boost::system::error_code& ec) {
                    if (ec)
                    {
                        cerr << "Call FruDevice ReScanBus failed.\n";
                        setFruGpio(0);
                        return;
                    }
                    // Switch the eeprom device back to HOST through gpio.
                    setFruGpio(0);

                    bus->async_method_call(
                        [](const boost::system::error_code& ec) {
                            if (ec)
                            {
                                cerr << "Call EntityManager ReScan failed.\n";
                                return;
                            }
                        },
                        "xyz.openbmc_project.EntityManager",
                        "/xyz/openbmc_project/EntityManager",
                        "xyz.openbmc_project.EntityManager", "ReScan");
                },
                "xyz.openbmc_project.FruDevice",
                "/xyz/openbmc_project/FruDevice",
                "xyz.openbmc_project.FruDeviceManager", "ReScanBus", i2cBus);

            break;
        }

        memcpy(temporary.data() + addrSize, fruRawBinary.data() + writeOffset,
               pageSize);

        if (write(fruOut, temporary.data(), pageSize + addrSize) !=
            pageSize + addrSize)
        {
            cerr << "Error write.\n";
            close(fruOut);
            setFruGpio(0);
            return ipmi::responseUnspecifiedError();
        }

        uint16_t address16 = static_cast<uint16_t>(index * pageSize);

        temporary[0] = address16 >> 8;
        temporary[1] = address16 & 0xff;

        fruSize -= pageSize;
        writeOffset += pageSize;

        // most eeproms require 5-10ms between writes
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    return ipmi::responseSuccess();
}

static void register_fru_functions() __attribute__((constructor));
static void register_fru_functions()
{
    ipmi::registerHandler(ipmi::prioOemBase, ipmi::netFnOemSix,
                          oem::cmdOemWriteFru, ipmi::Privilege::User,
                          ipmiOemWriteFru);
    return;
}
